
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.Diagnostics;
using System.Text;
using System.Threading;

using Swaf;
using Swaf.BizObj;
using Swaf.Config;
using Swaf.Container;
using Swaf.Security;


namespace UserLogin
{
	/// <summary>
	/// This class works as a CSF dynamic security IResourceSetPartner and manages various
    /// in memory collections of user login objects such as active user groups, the current
    /// set of application functions, and which security principals are referring to each active
    /// user group.  It is also a config partner to process startup Application Function
    /// configuration options.</summary>
	/// <remarks>
	/// This is the configuration partner that processes the AppFunctions startup
	/// config options and manages the in memory collection of current application functions.
	/// 
	/// It also maintains in memory references to active user groups, ISecurityPrincipal instances
	/// and the user objects contained within them so that as the user login business rules
	/// are called to modify them (such as changing the security privileges for a specific
	/// user group) they can be updated in memory as well.  This allows changes to be visible
	/// to active users without them logging off.  For example, if an administrator changed the
	/// security privilege level for a user group and there are 4 users logged in at the time,
	/// those users will start using the changed security privilege right away.
	/// </remarks>
    class AppFunctionConfig : IConfigPartner, IResourceSetPartner, IUserLoginManager
    {
        protected IBizObjList m_globalAppFunctions;
		protected FlexiMap m_companySpecificAppFunctions = new FlexiMap();
        protected IBizObjList m_dynamicAppFunctionSpec;

		protected IDictionary m_activeUserGroups = new ThreadSafeHashtable();
		protected IDictionary m_activeUsers = new ThreadSafeHashtable();
		protected string m_companySpecificNodeType = null;

		protected IServerStateManager m_stateMgr = null;

		/// <summary>
		/// Used only during startup to manage app function definitions until startup is 
		/// complete and bizobjs can be created.
		/// </summary>
        protected IList m_startupAppFunctions = new ArrayList();

        public AppFunctionConfig(IApplication app)
        {
        }

        #region IUserLoginManager Members
        /// <summary>
        /// 
        /// </summary>
        /// <remarks> This approach of copying values from the source group to the active group
        /// is not checking for fields that are IBizObj or IBizObjList and will simply put the IBizObj
        /// or IBizObj list directly into the active group.  This means those types will be shared
        /// between the two groups.</remarks>
        /// <param name="group">The source group with changes to apply if the group is actively in memory.</param>
        public void userGroupUpdated(IBizObj group)
        {
            ActiveUserGroup info = (ActiveUserGroup)m_activeUserGroups[group.get("id")];
            if (info != null)
            {
				info.Lock.AcquireWriterLock(-1);
				try
				{
					IBizObjDefinition def = info.UserGroup.type;
					foreach (IBizObjField fld in def.FieldList)
						info.UserGroup.put(fld.Name, group.get(fld.Name));
					foreach (ISecurityPrincipal principal in info.Principals)
					{
						Debug.Assert(principal.resourceSets.get("AppFunctions") != null);
						updateResources((IPartneredResourceSet)principal.resourceSets.get("AppFunctions"), principal);
					}
				}
				finally
				{
					info.Lock.ReleaseWriterLock();
				}
            }
        }

		public void userGroupDeleted(IBizObj group)
		{
			ActiveUserGroup info = (ActiveUserGroup)m_activeUserGroups[group.get("id")];
			if (info != null)
			{
				info.Lock.AcquireWriterLock(-1);
				try
				{
					foreach (ISecurityPrincipal principal in info.Principals)
					{
						IBizObjList userGroups = (IBizObjList)principal.additionalInfo.get("groups");
						int index = 0;
						foreach (IBizObj userGroup in userGroups)
						{
							if (userGroup.get("id").Equals(group.get("id")))
							{
								userGroups.removeAt(index);
								break;
							}
							index++;
						}
						Debug.Assert(principal.resourceSets.get("AppFunctions") != null);
						updateResources((IPartneredResourceSet)principal.resourceSets.get("AppFunctions"), principal);
					}
				}
				finally
				{
					info.Lock.ReleaseWriterLock();
				}
			}
		}

        public IBizObj userActive(IBizObj user, IBizObjList userGroups)
        {
            IList userUserGroups = (IList)user.get("groups");
            userUserGroups.Clear();
            foreach (IBizObj group in userGroups)
                userUserGroups.Add(findActiveUserGroup(group, true));

            return user;
        }

        public void userUpdated(IBizObj user, IBizObjList userGroups)
        {
            ISecurityPrincipal principal = (ISecurityPrincipal)m_activeUsers[user.get("id")];
            if (principal != null)
            {
                userActive(user, userGroups);
                Debug.Assert(principal is DynamicResourcesSecurityPrincipal);
                DynamicResourcesSecurityPrincipal dp = (DynamicResourcesSecurityPrincipal)principal;
                user.put("companyId", dp.additionalInfo.get("companyId"));
                user.put("activeCompanyId",dp.additionalInfo.get("activeCompanyId"));
                user.put("authenticated", dp.additionalInfo.get("authenticated"));

                dp.updateAdditionalInfo(user);
            }
        }

        public void userDeactive(IBizObj user)
        {
            IList userUserGroups = (IList)user.get("groups");
            foreach (IBizObj group in userUserGroups)
            {
                ActiveUserGroup info = (ActiveUserGroup)m_activeUserGroups[group.get("id")];
                Debug.Assert(info != null);
				info.Lock.AcquireWriterLock(-1);
				try
				{
					if (--info.UserCount == 0)
					{
						m_activeUserGroups.Remove(group.get("id"));
					}
				}
				finally
				{
					info.Lock.ReleaseWriterLock();
				}
            }
        }

        public void principalActive(ISecurityPrincipal principal)
        {
            IBizObj user = (IBizObj)principal.additionalInfo;
            foreach (IBizObj userGroup in (IList)user.get("groups"))
            {
                ActiveUserGroup activeGroup = (ActiveUserGroup)m_activeUserGroups[userGroup.get("id")];
                Debug.Assert(activeGroup != null);
				activeGroup.Lock.AcquireWriterLock(-1);
				try
				{
					activeGroup.Principals.Add(principal);
				}
				finally
				{
					activeGroup.Lock.ReleaseWriterLock();
				}
            }
            m_activeUsers[user.get("id")] = principal;
            //Calling this here to ensure that the company the user is from has their app functions loaded before
            //the user starts to edit content.  This solves a bug that cause an app function created from data to be
            //added twice if the app functions for the company were loaded at the time the app function was created.
            //By doing this now, it ensure the app functions for a company are loaded when the user creates data
            //that is also an app function too.
            IBizObjList appFuns = AppFunctions;
        }

        public void principalDeactive(ISecurityPrincipal principal)
        {
            IBizObj user = (IBizObj)principal.additionalInfo;
            foreach (IBizObj userGroup in (IList)user.get("groups"))
            {
                ActiveUserGroup activeGroup = (ActiveUserGroup)m_activeUserGroups[userGroup.get("id")];
                Debug.Assert(activeGroup != null);
				activeGroup.Lock.AcquireWriterLock(-1);
				try
				{
					activeGroup.Principals.Remove(principal);
				}
				finally
				{
					activeGroup.Lock.ReleaseWriterLock();
				}
            }

            m_activeUsers.Remove(user.get("id"));
        }

        public void appFunctionAdded(string appFunctionGroupId, IBizObj appFunction)
        {
            IBizObj funGroup = getAppFunctionGroup(appFunctionGroupId, null, false);
            if (funGroup == null)
            {
            }
            ((IBizObjList)funGroup.get("appFunctions")).insertNewMakeCopy(appFunction);
        }

		public void appFunctionAdded(string appFunctionGroupId, IBizObj appFunction, Swaf.BGuid.Guid companyId)
		{
			IBizObj funGroup = getAppFunctionGroup(appFunctionGroupId, null, false, companyId);
			if (funGroup == null)
			{
			}
			((IBizObjList)funGroup.get("appFunctions")).insertNewMakeCopy(appFunction);
		}

        public void appFunctionRemoved(IBizObj appFunction)
        {
            string funId = (string)appFunction.get("id");
            for (int p = 0; p < m_globalAppFunctions.numElements; ++p)
            {
                if ((string)m_globalAppFunctions[p].get("id") == funId)
                {
                    m_globalAppFunctions.removeAt(p);
                    break;
                }
            }

            foreach (ActiveUserGroup info in m_activeUserGroups.Values)
            {
				info.Lock.AcquireReaderLock(-1);
				try
				{
					IBizObjList appFunctions = (IBizObjList)info.UserGroup.get("appPrivileges");
					for (int p = 0; p < appFunctions.numElements; ++p)
					{
						if ((string)appFunctions[p].get("id") == funId)
						{
							appFunctions.removeAt(p);
							foreach (ISecurityPrincipal principal in info.Principals)
								updateResources((IPartneredResourceSet)principal.resourceSets.get("AppFunctions"), principal);
							break;
						}
					}
				}
				finally
				{
					info.Lock.ReleaseReaderLock();
				}
            }
        }

        public IBizObjList AppFunctions
        {
            get 
			{
				IBizObjList appFunctions = null;
				ISecurityPrincipal princ = Application.currentApp.securityMgr.currentPrincipal;
				if (princ == null)
					appFunctions = m_globalAppFunctions;
				else
				{
					Debug.Assert(princ.additionalInfo is IBizObj);
					IBizObj user = (IBizObj)princ.additionalInfo;
					Debug.Assert(user.isKindOf("User"));
                    
                    Swaf.BGuid.Guid companyId = (Swaf.BGuid.Guid)user.get("activeCompanyId");
					appFunctions = (IBizObjList)m_companySpecificAppFunctions[companyId.ToString()];
					if (appFunctions == null)
					{
						m_companySpecificAppFunctions.put(companyId.ToString(), appFunctions =
							Application.globalApp.bizObjMgr.createSource(SourceType.BasicSource, "").getList("AppFunctionGroup", ""),
							m_companySpecificNodeType);

						foreach (IBizObj funGroup in m_globalAppFunctions)
							((IList)appFunctions).Add(funGroup.Clone());

						foreach (IBizObj dynAppFunction in m_dynamicAppFunctionSpec)
						{
							if ((string)dynAppFunction.get("scope") != "global")
							{
								IBizObj funGroup = getAppFunctionGroup((string)dynAppFunction.get("appFunctionGroupId"),
									(string)dynAppFunction.get("appFunctionGroupDescription"), true, companyId);
								setupFromDynamicFunctionSpec(funGroup, dynAppFunction);
							}
						}
					}
				}

				return appFunctions;
			}
        }

		public IBizObjList getCompanyAppFunctions(Swaf.BGuid.Guid companyId)
		{
			IBizObjList flatAppfunctionList = Application.currentApp.bizObjMgr.createList("AppFunction");
			IBizObjList companyAppGroups = AppFunctions;
			foreach(IBizObj funGroup in companyAppGroups)
				addFunctionsFromGroup(flatAppfunctionList, funGroup);

			//foreach (IBizObj funGroup in m_globalAppFunctions)
			//    addFunctionsFromGroup(flatAppfunctionList, funGroup);

			//foreach (IBizObj dynAppFunction in m_dynamicAppFunctionSpec)
			//{
			//    if ((string)dynAppFunction.get("scope") != "global")
			//    {
			//        IBizObj funGroup = getAppFunctionGroup((string)dynAppFunction.get("appFunctionGroupId"),
			//            (string)dynAppFunction.get("appFunctionGroupDescription"), false, companyId);
			//        //setupFromDynamicFunctionSpec(funGroup, dynAppFunction);
			//        addFunctionsFromGroup(flatAppfunctionList, funGroup);
			//    }
			//}
			
			return flatAppfunctionList;
		}
		protected void addFunctionsFromGroup(IBizObjList funList, IBizObj group)
		{
			IBizObj groupFun = funList.insertNew();
			groupFun.put("id", group.get("id"));
			groupFun.put("description", group.get("description"));
			foreach (IBizObj fun2 in group.get("appFunctions") as IBizObjList)
			{
				IBizObj fun = (IBizObj)fun2.Clone();
				fun.put("depth", 1);
				funList.Add(fun);
			}
		}
        #endregion


        #region IConfigPartner Members

        public void checkConfigStatus()
        {
            m_dynamicAppFunctionSpec = Application.globalApp.bizObjMgr.createSource(SourceType.BasicSource, "").getList("DynamicAppFunctionSpecifier", "");
            m_globalAppFunctions = Application.globalApp.bizObjMgr.createSource(SourceType.BasicSource, "").getList("AppFunctionGroup", "");
			m_companySpecificNodeType = (string)Application.globalApp.appData.get("userLoginOptions.companySpecificNodeType");
            foreach (string[] info in m_startupAppFunctions)
            {
                if (info[0] == "A")
                {
                    Debug.Assert(info.Length == 5);
                    IBizObj funGroup = getAppFunctionGroup(info[1], info[2], true);
                    IBizObj appFunction = ((IBizObjList)funGroup.get("appFunctions")).insertNew();
                    appFunction.put("id", info[3]);
                    appFunction.put("description", info[4]);
                }
                else //currently only other possibility: if info[0] == "D"
                {
                    Debug.Assert(info.Length == 7);
                    Debug.Assert(info[0] == "D");
                    IBizObj dynAppFunction = m_dynamicAppFunctionSpec.insertNew();
					dynAppFunction.put("appFunctionGroupId", info[1]);
					dynAppFunction.put("appFunctionGroupDescription", info[2]);
					dynAppFunction.put("id", info[3]);
                    dynAppFunction.put("bizrule", info[4]);
                    dynAppFunction.put("bizobjType", info[5]);
					dynAppFunction.put("scope", info[6]);
					if (info[6].ToLower() == "global")
					{
						IBizObj funGroup = getAppFunctionGroup(info[1], info[2], true);
						setupFromDynamicFunctionSpec(funGroup, dynAppFunction);
					}
                }
            }
            m_startupAppFunctions = null;
        }

        public void registerForConfig(IConfigManager config)
        {
            config.registerHandler("...<ApplicationFunctions>", new AppFunctionGroupConfigHandler(this));
            config.registerHandler("...<ApplicationFunctions> <AppFunction>", new AppFunctionsConfigHandler(this));
            config.registerHandler("...<ApplicationFunctions> <DynamicAppFunction>", new DynamicAppFunctionConfigHandler(this));
			config.registerHandler("...<UserLogin> <MulticastStateManagement>", new ServerStateConfigHandler(this));
        }

        #endregion

		protected IBizObj getAppFunctionGroup(string id, string desc, bool insertIfNotFound)
		{
			ISecurityPrincipal princ = Application.currentApp.securityMgr.currentPrincipal;
			Swaf.BGuid.Guid companyId = null;
			if(princ.additionalInfo != null)
				companyId = (Swaf.BGuid.Guid)princ.additionalInfo.get("activeCompanyId");
			return getAppFunctionGroup(id, desc, insertIfNotFound, companyId);
		}

        protected IBizObj getAppFunctionGroup(string id, string desc, bool insertIfNotFound, Swaf.BGuid.Guid companyId)
        {
			IBizObjList appFunctions = null;
			if (companyId == null)
				appFunctions = m_globalAppFunctions;
			else
			{
				appFunctions = AppFunctions;
                /*
                (IBizObjList)m_companySpecificAppFunctions[companyId.ToString()];
				if (appFunctions == null)
					m_companySpecificAppFunctions.put(companyId.ToString(), appFunctions = 
						Application.globalApp.bizObjMgr.createSource(SourceType.BasicSource, "").getList("AppFunctionGroup", ""),
						m_companySpecificNodeType);
                */
			}

            Debug.Assert(appFunctions != null);
            IBizObj funGroup = null;
            foreach (IBizObj group in appFunctions)
            {
                if ((string)group.get("id") == id)
                {
                    funGroup = group;
                    break;
                }
            }
            if (funGroup == null && insertIfNotFound)
            {
                funGroup = appFunctions.insertNew();
                funGroup.put("id", id);
                funGroup.put("description", desc);
            }
            return funGroup;
        }

        protected void setupFromDynamicFunctionSpec(IBizObj funGroup, IBizObj funSpec)
        {
            string bizrule = (string)funSpec.get("bizrule");
            if (bizrule == null)
                throw new UserLoginException(string.Format("bizrule is not specified on dynamicAppFunction named '{0}'", funSpec.get("id")));

            try
            {
                IResults rc = Application.currentApp.ruleMgr.execute(bizrule, funSpec.get("id"));
                if (rc.CallResults != null && rc.CallResults is IBizObjList)
                {
                    IBizObjList groupFunctions = (IBizObjList)funGroup.get("appFunctions");
                    IBizObjList appFunctions = (IBizObjList)rc.CallResults;
                    foreach (IBizObj appFunction in appFunctions)
                        groupFunctions.Add(appFunction);
                }
            }
            catch (Exception e)
            {
                throw new UserLoginException(string.Format("Error calling DynamicAppFunction '{0}' bizrule '{1}'", funSpec.get("id"), bizrule), e);
            }
        }

        protected IBizObj findActiveUserGroup(IBizObj group, bool addIfNotFound)
        {
            IBizObj userGroup = group;
            ActiveUserGroup info = (ActiveUserGroup)m_activeUserGroups[group.get("id")];
            if (info == null && addIfNotFound)
                m_activeUserGroups[group.get("id")] = new ActiveUserGroup(group);
            else
            {
				info.Lock.AcquireWriterLock(-1);
				try
				{
					IBizObjDefinition def = group.type;
					foreach (IBizObjField fld in def.FieldList)
						info.UserGroup.put(fld.Name, group.get(fld.Name));

					if (addIfNotFound)
						++info.UserCount;
				}
				finally
				{
					info.Lock.ReleaseWriterLock();
				}
                userGroup = info.UserGroup;
					
            }
            return userGroup;
        }

        protected void registerStaticAppFunction(string groupId, string groupDesc, string id, string description)
        {
            m_startupAppFunctions.Add(new string[] {"A", groupId, groupDesc, id, description });
        }

        protected void registerDynamicAppFunction(string groupId, string groupDesc, string id, string bizrule, string bizobjType, string scope)
        {
            m_startupAppFunctions.Add(new string[] { "D", groupId, groupDesc, id, bizrule, bizobjType, scope });
        }

		protected void setupStateManagement(string id, string multicastIP, int port, int hops, string cryptoKeyName)
		{
			m_stateMgr = new UdpServerStateManager(id, multicastIP, port, hops, cryptoKeyName);
			m_stateMgr.registerMessageHandler("userActive", new processMessage(stateMgrUserActive));
			m_stateMgr.registerMessageHandler("userInactive", new processMessage(stateMgrUserInactive));
			m_stateMgr.registerMessageHandler("usergroupDeleted", new processMessage(stateMgrUsergroupDeleted));
			m_stateMgr.registerMessageHandler("usergroupUpdated", new processMessage(stateMgrUsergroupUpdated));
			m_stateMgr.registerMessageHandler("userUpdated", new processMessage(stateMgrUserUpdated));
			m_stateMgr.registerMessageHandler("appFunAdded", new processMessage(stateMgrAppFunAdded));
			m_stateMgr.registerMessageHandler("appFunRemoved", new processMessage(stateMgrAppFunRemoved));
		}

		protected void stateMgrUserActive(string key, object data)
		{
		}

		protected void stateMgrUserInactive(string key, object data)
		{
		}

		protected void stateMgrUsergroupDeleted(string key, object data)
		{
		}

		protected void stateMgrUsergroupUpdated(string key, object data)
		{
		}

		protected void stateMgrUserUpdated(string key, object data)
		{
		}

		protected void stateMgrAppFunAdded(string key, object data)
		{
		}

		protected void stateMgrAppFunRemoved(string key, object data)
		{
		}


		protected class ActiveUserGroup
        {
			public ReaderWriterLock Lock = new ReaderWriterLock();
            public int UserCount = 0;
            public IBizObj UserGroup = null;
            public IList Principals = new ArrayList();

            public ActiveUserGroup(IBizObj group) { UserGroup = group; }
        }

        #region ConfigHandlers
		private class ServerStateConfigHandler : ConfigHandler
		{
			private static string[] s_legalAttributes = new string[]{"id","multicastIP","port","hops", "cryptoKey"};
            private AppFunctionConfig m_parent;
            public ServerStateConfigHandler(AppFunctionConfig parent)
            {
                m_parent = parent;
            }

            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);
				string id = configInfo.getAttribute("id");
				string multicastIP = configInfo.getAttribute("multicastIP");
				int port = 11000;
				int hops = 1;
				try
				{
					hops = Int32.Parse(configInfo.getAttribute("hops", "1"));
					port = Int32.Parse(configInfo.getAttribute("port", "11000"));
				}
				catch(Exception e)
				{
					throw new UserLoginException("Unable to parse hops/port on server state configuration of user login", e);
				}
				string cryptoKey = configInfo.getAttribute("cryptoKey");
				m_parent.setupStateManagement(id, multicastIP, port, hops, cryptoKey);
            }
		}

        private class AppFunctionGroupConfigHandler : ConfigHandler
        {
            private static string[] s_legalAttributes = new string[]{"id","description"};
            private AppFunctionConfig m_parent;

            public AppFunctionGroupConfigHandler(AppFunctionConfig parent)
            {
                m_parent = parent;
            }

            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);
                configInfo.Context["AppFunctionGroupId"] = configInfo.getAttribute("id", "");
                configInfo.Context["AppFunctionGroupDescription"] = configInfo.getAttribute("description", "");
            }

            public override void executeAfter(IConfigElement configInfo)
            {
                configInfo.Context.Remove("AppFunctionGroupId");
                configInfo.Context.Remove("AppFunctionGroupDescription");
            }
        }

        private class AppFunctionsConfigHandler : ConfigHandler
        {
            private static string[] s_legalAttributes = new string[]{"id","description"};
            private AppFunctionConfig m_parent;

            public AppFunctionsConfigHandler(AppFunctionConfig parent)
            {
                m_parent = parent;
            }

            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);
                m_parent.registerStaticAppFunction((string)configInfo.Context["AppFunctionGroupId"], (string)configInfo.Context["AppFunctionGroupDescription"],
                    configInfo.getAttribute("id"), configInfo.getAttribute("description"));
            }
        }

        private class DynamicAppFunctionConfigHandler : ConfigHandler
        {
            private static string[] s_legalAttributes = new string[]{"id","bizrule","bizobjType","scope"};
            private AppFunctionConfig m_parent;

            public DynamicAppFunctionConfigHandler(AppFunctionConfig parent)
            {
                m_parent = parent;
            }

            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);
                m_parent.registerDynamicAppFunction((string)configInfo.Context["AppFunctionGroupId"], (string)configInfo.Context["AppFunctionGroupDescription"],
                    configInfo.getAttribute("id"), configInfo.getAttribute("bizrule"), configInfo.getAttribute("bizobjType"),
					configInfo.getAttribute("scope"));
            }
        }
        #endregion


        #region IResourceSetPartner Members
 
        public void updateResources(IPartneredResourceSet resourceSet, ISecurityPrincipal principal)
        {
            INameAddressSupport userResources = new FlexiMap();
            if (principal != null && principal.additionalInfo is IBizObj)
            {
                IBizObj user = (IBizObj)principal.additionalInfo;
                IResults rc = Application.currentApp.ruleMgr.execute("getUserAppFunctionPrivilages", user);
                if (rc.CallResults != null && rc.CallResults is IBizObjList)
                {
                    IBizObjList appFunctions = (IBizObjList)rc.CallResults;
                    foreach (IBizObj appFunction in appFunctions)
                    {
                        string id = (string)appFunction.get("id").ToString();
                        userResources.put(id, new AppFunctionResource(id,
                            (Privileges)(int)appFunction.get("privilege")));
                    }
                }
            }

            resourceSet.resetResources(userResources);
            
        }

        #endregion
    }
}
